package com.hedl.classroom.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hedl.classroom.base.exception.CommonResult;
import com.hedl.classroom.base.exception.OnlinecClassroomException;
import com.hedl.classroom.base.model.PageParams;
import com.hedl.classroom.base.model.PageResult;
import com.hedl.classroom.mapper.MediaFilesMapper;
import com.hedl.classroom.mapper.MediaProcessMapper;
import com.hedl.classroom.model.dto.QueryMediaParamsDto;
import com.hedl.classroom.model.dto.UploadFileParamsDto;
import com.hedl.classroom.model.dto.UploadFileResultDto;
import com.hedl.classroom.model.po.MediaFiles;
import com.hedl.classroom.model.po.MediaProcess;
import com.hedl.classroom.service.MediaFileService;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import io.minio.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.io.*;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;


@Slf4j
@Service
public class MediaFileServiceImpl implements MediaFileService {
     @Resource
     private MediaFilesMapper mediaFilesMapper;

     @Resource
     private MinioClient minioClient;

     @Resource
     private MediaProcessMapper mediaProcessMapper;

     //存取普通文件
     @Value("${minio.bucket.files}")
     private String media_files;

     //存取视频
     @Value("${minio.bucket.video}")
     private String media_video;

     @Override
     public MediaFiles getFileById(String mediaId) {
          if(mediaId == null)
               OnlinecClassroomException.cast("媒资id为空！");

          return mediaFilesMapper.selectById(mediaId);
     }

     @Override
     public PageResult<MediaFiles> queryMediaFiels(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {

          //构建查询条件对象
          LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();
          queryWrapper.orderByDesc(MediaFiles::getChangeDate);
          //queryWrapper.like(!StringUtils.isEmpty(queryMediaParamsDto.getFilename()),MediaFiles::getFilename,queryMediaParamsDto.getFilename());
          //queryWrapper.eq(!StringUtils.isEmpty(queryMediaParamsDto.getFileType()), MediaFiles::getFileType,queryMediaParamsDto.getFileType());

          //分页对象
          Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
          // 查询数据内容获得结果
          Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
          // 获取数据列表
          List<MediaFiles> list = pageResult.getRecords();
          // 获取数据总数
          long total = pageResult.getTotal();
          // 构建结果集
          PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
          return mediaListResult;

     }


     @Override
     @Transactional
     public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
          String fileMd5 = DigestUtils.md5Hex(bytes);
          if(StringUtils.isEmpty(folder)){
               // 如果目录不存在，则自动生成一个目录
               folder = getFileFolder(true,true,true);
          }else if(!folder.endsWith("/")){
               // 如果目录末尾没有 / ，替他加一个
               folder = folder + "/";
          }
          if (StringUtils.isEmpty(objectName)){
               // 如果文件名为空，则设置其默认文件名为文件的md5码 + 文件后缀名
               String filename = uploadFileParamsDto.getFilename();
               objectName = fileMd5 + filename.substring(filename.lastIndexOf("."));
          }
          objectName = folder + objectName;
          try{
               addMediaFilesToMinIO(bytes,media_files,objectName);
               MediaFiles mediaFiles = addMediaFilesToDB(companyId, uploadFileParamsDto, objectName, fileMd5, media_files);
               UploadFileResultDto resultDto = new UploadFileResultDto();
               BeanUtils.copyProperties(mediaFiles,resultDto);
               return resultDto;
          }catch (Exception e){
               OnlinecClassroomException.cast("上传过程中出错了！");
          }
          return null;
     }


     @Override
     @Transactional(propagation = Propagation.REQUIRED)
     public MediaFiles addMediaFilesToDB(Long companyId,
                                          UploadFileParamsDto uploadFileParamsDto,
                                          String objectName, String fileMD5, String bucket){
          MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMD5);
          if(mediaFiles == null){
               mediaFiles = new MediaFiles();
               BeanUtils.copyProperties(uploadFileParamsDto,mediaFiles);
               mediaFiles.setId(fileMD5);
               mediaFiles.setFileId(fileMD5);
               mediaFiles.setCompanyId(companyId);
               mediaFiles.setBucket(bucket);
               mediaFiles.setCreateDate(LocalDateTime.now());
               mediaFiles.setChangeDate(LocalDateTime.now());
               mediaFiles.setStatus("1");
               mediaFiles.setFilePath(objectName);
               mediaFiles.setUrl("/"+bucket+"/"+objectName);
               //查阅数据字典，002003表示审核通过
               mediaFiles.setAuditStatus("002003");
          }
          int insert = mediaFilesMapper.insert(mediaFiles);
          if(insert <= 0){
               OnlinecClassroomException.cast("保存文件信息失败！");
          }
          //记录待处理任务
          addWaitingTask(mediaFiles);

          //向 MediaProcess插入记录

          return mediaFiles;
     }


     @Override
     public CommonResult<Boolean> checkFile(String fileMd5) {
          //先查询数据库
          MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
          //如果数据存在在查询minio
          if (mediaFiles != null){
               try {
                    //查询远程服务获取一个流对象
                    /*
                    InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
                            .bucket(mediaFiles.getBucket())
                            .object(mediaFiles.getFilePath())
                            .build());
                    if (inputStream != null){
                         //文件以存在
                         return CommonResult.success(true);
                    }
                     */
                    StatObjectResponse statObjectResponse = minioClient.statObject(StatObjectArgs.builder()
                            .bucket(mediaFiles.getBucket()).object(mediaFiles.getFilePath()).build());
                    if (statObjectResponse == null){
                         return CommonResult.success(false);
                    }
               } catch (Exception e) {
                    //e.printStackTrace();
                    return CommonResult.success(false);
               }
          }
          //文件不存在
          return CommonResult.success(true);
     }

     @Override
     public CommonResult<Boolean> checkChunk(String fileMd5, int chunkIndex) {
          //分块存储路径是：md5前面两位为目录，chunk存储分块文件
          String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
          //根据md5得到分块文件的路径
          String chunkFilePath = chunkFileFolderPath + chunkIndex;
          try {
               //如果数据存在在查询minio,查询远程服务获取一个流对象
               /*
               InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
                       .bucket(media_video)
                       .object(chunkFilePath)
                       .build());
               if (inputStream != null){
                    //文件以存在
                    return CommonResult.success(true);
               }
                */
               StatObjectResponse statObjectResponse = minioClient.statObject(StatObjectArgs.builder()
                       .bucket(media_video).object(chunkFilePath).build());
               log.info("返回的 信息:{}",statObjectResponse);
               if (statObjectResponse == null){
                    //文件以存在
                    return CommonResult.success(false);
               }
               //文件以存在
               //return CommonResult.success(exist);
          } catch (Exception e) {
               //e.printStackTrace();
               return CommonResult.success(false);

          }
          //文件不存在
          return CommonResult.success(true);
     }

     @Override
     public CommonResult uploadChunk(String fileMd5, int chunk, byte[] bytes) {
          //分块文件的路径
          String chunkFilePath = getChunkFileFolderPath(fileMd5) + chunk;
          try{
               //将分块上传到minio
               addMediaFilesToMinIO(bytes,media_video,chunkFilePath);
               return CommonResult.success(true);
          }catch (Exception e){
               log.debug("上传分块文件：{}失败：{}", chunkFilePath, e.getMessage());
          }
          return CommonResult.validfail(false,"上传分块文件失败！");
     }

     @Override
     public CommonResult mergeChunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto){
          //下载分块文件
          File[] chunkFiles = checkChunkStatus(fileMd5, chunkTotal);
          //获取原文件名
          String fileName = uploadFileParamsDto.getFilename();
          //获取源文件扩展名
          String extension = fileName.substring(fileName.lastIndexOf("."));
          //创建出临时文件，准备合并
          File mergeFile = null;
          try{
               mergeFile = File.createTempFile(fileName, extension);
          }catch (IOException e){
               OnlinecClassroomException.cast("创建合并临时文件出错");
          }
          try{
               //缓存区
               byte[] buffer = new byte[1024];
               //写入流，向临时文件写入
               try(RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw")){
                    //遍历分块文件数组
                    for(File chunkFile : chunkFiles){
                         //读取流，读分块文件
                         try(RandomAccessFile raf_read = new RandomAccessFile(chunkFile, "r")){
                              int len;
                              while((len = raf_read.read(buffer)) != -1){
                                   raf_write.write(buffer,0,len);
                              }
                         }
                    }
               }catch (Exception e){
                    OnlinecClassroomException.cast("合并文件过程中出错");
               }
               uploadFileParamsDto.setFileSize(mergeFile.length());
               //对文件进行校验，通过md5值比较
               try(FileInputStream mergeInputStream = new FileInputStream(mergeFile)){
                    String mergeMd5 = DigestUtils.md5Hex(mergeInputStream);
                    if (!fileMd5.equals(mergeMd5)){
                         OnlinecClassroomException.cast("合并文件校验失败");
                    }
                    log.debug("合并文件校验失败：{}",mergeFile.getAbsolutePath());
               }catch (Exception e){
                    OnlinecClassroomException.cast("合并文件校验异常");
               }

               //拼接合并文件路径
               String mergeFilePath = getFilePathByMd5(fileMd5, extension);
               //将本地合并好的文件，上传到minio中，这里重载了一个方法
               addMediaFilesToMinIO(mergeFile.getAbsolutePath(),media_video,mergeFilePath);
               log.debug("合并文件上传至MinIO完成{}",mergeFile.getAbsolutePath());
               //将文件信息写入数据库
               MediaFiles mediaFiles = addMediaFilesToDB(companyId, uploadFileParamsDto, mergeFilePath, fileMd5, media_video);
               if (mediaFiles == null){
                    OnlinecClassroomException.cast("媒资文件入库出错");
               }
               log.debug("媒资文件入库完成");
               return CommonResult.success();
          }finally {
               for (File chunkFile : chunkFiles) {
                    try{
                         chunkFile.delete();
                    }catch (Exception e){
                         log.debug("临时分块文件删除错误：{}",e.getMessage());
                    }
               }
               try{
                    mergeFile.delete();
               }catch (Exception e){
                    log.debug("临时合并文件删除错：{}",e.getMessage());
               }
          }

     }

     @Override
     public File downloadFileFromMinio(File file,String bucket,String objectName){
          try(FileOutputStream outputStream = new FileOutputStream(file);
              InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
                      .bucket(bucket)
                      .object(objectName)
                      .build())){
               IOUtils.copy(inputStream,outputStream);
               return file;
          }catch (Exception e){
               OnlinecClassroomException.cast("查询文件分块出错");
          }
          return null;
     }

     /**
      * 添加待处理任务
      * @param mediaFiles 媒资文件信息
      */
     private void addWaitingTask(MediaFiles mediaFiles){
          //获取文件的mimetType
          String filename = mediaFiles.getFilename();
          String mimeType = getContentType(filename);
          if(mimeType.equals("video/x-msvideo")){      //如果是avi视频写入待处理任务
               MediaProcess mediaProcess = new MediaProcess();
               BeanUtils.copyProperties(mediaFiles,mediaProcess);
               //状态是未处理
               mediaProcess.setStatus("1");
               mediaProcess.setCreateDate(LocalDateTime.now());
               mediaProcess.setFailCount(0);      //失败次数默认为：0
               mediaProcess.setUrl(null);
               mediaProcessMapper.insert(mediaProcess);

          }
          //通过mimetType判断如果是avi视频写入待处理任务
     }

     /**
      * 获取文件类型并上传到minio服务器
      * @param bytes 文件字节数据
      * @param bucket 桶
      * @param objectName 文件对象名称
      */
     private void addMediaFilesToMinIO(byte[] bytes, String bucket, String objectName) {
          ByteArrayInputStream byteArrayIntputStream = new ByteArrayInputStream(bytes);
          String contentType = getContentType(objectName);
          try {
               minioClient.putObject(PutObjectArgs.builder()
                       .bucket(bucket)
                       .object(objectName)
                       .stream(byteArrayIntputStream,byteArrayIntputStream.available(),-1)
                       .contentType(contentType)
                       .build());
          } catch (Exception e) {
               log.debug("上传到文件系统出错:{}", e.getMessage());
               throw new OnlinecClassroomException("上传到文件系统出错！");
          }
     }

     /**
      * 通过年月日分解，组装文件路径目录
      * @param year 一级路径
      * @param month 二级路径
      * @param day 三级路径
      * @return
      */
     private String getFileFolder(boolean year, boolean month, boolean day) {
          StringBuffer stringBuffer = new StringBuffer();
          SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
          String format = dateFormat.format(new Date());
          String[] split = format.split("-");
          if(year){
               stringBuffer.append(split[0]).append("/");
          }
          if (month){
               stringBuffer.append(split[1]).append("/");
          }
          if (day){
               stringBuffer.append(split[2]).append("/");
          }
          return stringBuffer.toString();
     }

     /**
      * 的到分块文件的目录
      * @param fileMd5 文件md5
      * @return
      */
     private String getChunkFileFolderPath(String fileMd5){
          return fileMd5.substring(0,1)+"/"+fileMd5.substring(1,2)+"/"+fileMd5+"/"+"chunk"+"/";
     }

     /**
      * 下载分块文件
      * @param fileMd5 文件的md5
      * @param chunkTotal 总分块数
      * @return 分块文件数组
      */
     private File[] checkChunkStatus(String fileMd5,int chunkTotal){
          //作为结果返回
          File[] files = new File[chunkTotal];
          //获取分块文件目录
          String chunkFileFolder = getChunkFileFolderPath(fileMd5);
          for (int i = 0; i < chunkTotal; i++){
               //获取分块文件路径
               String chunkFilePath = chunkFileFolder + i;
               File chunkFile = null;
               try {
                    //创建临时的分块文件
                    chunkFile = File.createTempFile("chunk" + i, null);
               } catch (Exception e) {
                    OnlinecClassroomException.cast("创建临时分块文件出错："+e.getMessage());
               }
               //下载分块文件
               chunkFile = downloadFileFromMinio(chunkFile, media_video, chunkFilePath);
               //组成结果
               files[i] = chunkFile;
          }
          return files;
     }

     /**
      * 将本地文件上传到minio
      * @param filePath      本地文件路径
      * @param bucket        桶
      * @param objectName    对象名称
      */
     private void addMediaFilesToMinIO(String filePath,String bucket,String objectName){
          String contentType = getContentType(objectName);
          try{
               minioClient.uploadObject(UploadObjectArgs.builder()
                       .bucket(bucket)
                       .object(objectName)
                       .filename(filePath)
                       .contentType(contentType)
                       .build());
          }catch (Exception e){
               OnlinecClassroomException.cast("上传到文件系统出错");
          }
     }

     private static String getContentType(String objectName){
          String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE; // 默认content-type为未知二进制流
          if (objectName.indexOf(".") >= 0) { // 判断对象名是否包含 .
               // 有 .  则划分出扩展名
               String extension = objectName.substring(objectName.lastIndexOf("."));
               // 根据扩展名得到content-type，如果为未知扩展名，例如 .abc之类的东西，则会返回null
               ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
               // 如果得到了正常的content-type，则重新赋值，覆盖默认类型
               if (extensionMatch != null) {
                    contentType = extensionMatch.getMimeType();
               }
          }
          return contentType;
     }

     /**
      * 根据MD5和文件扩展名，生成文件路径，例 /2/f/2f6451sdg/2f6451sdg.mp4
      * @param fileMd5 文件md5
      * @param fileExt 文件扩展名
      * @return
      */
     private String getFilePathByMd5(String fileMd5,String fileExt){
          return fileMd5.substring(0,1)+"/"+fileMd5.substring(1,2)+"/"+fileMd5+"/"+fileMd5+fileExt;
     }

     /**
      * 根据扩展名获取mimeType
      * @param extension
      * @return
      */
     private String getMimeType(String extension){
          if(extension == null){
               extension = "";
          }
          //根据扩展名取出mimeType
          ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
          String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType，字节流
          if(extensionMatch!=null){
               mimeType = extensionMatch.getMimeType();
          }
          return mimeType;

     }
}
